快速建模语言 Racket 概况

#Technomous #Lisp

现在让我们回到这一章的主题上来,这一章的题目叫做《字符与球拍》,现在就让我专门的来谈一谈“球拍”,即 Racket 语言。Racket 语言是由 Racket 开发小组开发的,其领导成员及核心开发成员之一是马修 · 弗拉特教授(Professor Matthew Flatt)。关于这个语言,我曾在 2015 年 11 月份采访过弗拉特教授,对这个语言的方方面面提出了约六十多个问题,他对这些问题都做了相应的回答。这个语言的设计初衷和现在的主流用户群体主要针对教育市场,在大学的教育中,Racket 开发小组想提供一种优美的、实用的Scheme 的实现。听到这里,有些创客可能会觉得失望,一种用于教育的语言可以运用在实际的研发项目上吗?答案是肯定的,Racket 语言有非常多的特性去非常好的满足各位的创业项目,在这里,让我来花点时间为大家讲解 Racket 语言。

Racket 语言是从 Scheme 语言社团内部发展起来的,Scheme 语言的语言规范是 R5RS,它对这个语言的内核部分进行了非常好的描述,这个语言定义的非常好,但在实用的角度来说此规范还有些不足,因为库函数(标准库的部分)没有被单独列出来,或者说它显得比较单薄。为了解决这个问题,Scheme 社团进行了大量的讨论,提出了 R6RS 这个 R5RS 的后一个版本,但这个语言规范推出之后,有很多人反对,因为 R6RS 的篇幅非常长,远远的超过了 R5RS, R5RS 用 A4 纸打印出来只有 50 多页,因此可以说 R6RS 是一次失败的尝试,因而又推出了 R7RS。R7RS 可以视为对 R5RS 的某种回归,当然在美国欧洲等信息技术较发达的地区来说,Scheme 可以形象地说她是编程语言王国的女皇,因此重视程度是非常高的,所以大家对这个语言规范怎么写有很大的分歧,站在不同的角度形成不同的观点,这是很正常的学术争论。但 Racket 开发小组认为不能仅仅停留在争论的层次中,要拿出实际的、可用的 Racket 实现,而且其应该拥有非常多的功能和特性来用于教学及各种目的的研发活动,这就是 Racket 语言诞生的基本背景。

Racket 项目的最初起源,即酝酿时期是在 1994 年,代码的编程工作开始于 1995 年,刚开始是想为学生学习 Scheme 提供一个友好的图形用户界面,在图形用户界面下内嵌一个 Scheme 求值器进行学习。一直发展到了 2015 年 11 月,当我采访 马修 · 弗拉特教授的时候,它的版本已经发展到了 6.2,去年 10 月份以后又推出了 6.3 版本,到目前我录课的时候版本应该是 6.3,以后它的版本肯定会不断升级,而且从以往的角度来看,Racket 小组一年要对版本进行四次比较大的升级。

对于 Racket 的用户而言,Racket 语言到底具有哪些主要的特征呢?去年 11 月份我采访弗拉特教授的时候,他归纳了三点:第一点是它有一个非常独特的宏的系统(Macro System),后面我再详细的展开一下;第二点是它具有合同系统,这是由罗比·菲德勒(Robby Findler)提出来的,有了合同系统后,Racket 对模块化的支持达到了新的高度,这样大型的软件项目可以分解成许多模块,模块与模块之间的关系我们可以通过合同来约定,就类似市场上的买卖双方签订一个合同,对于买卖、供需双方的传递与义务进行清晰的约定,这是一个很大的特征;第三个是在底层的抽象层次上,它提出了一个概念叫做 Custodian,是由 Custom 演变而来的,它可以认为是对进程的一种抽象,进程中管理着一些资源,还有一种叫做 Chaperones,这一种可以被视为 Bootstrap,即把这个合同系统启动起来的一个代理层次,这个特性在底层被设计的非常好,从用户编程的角度来看,每一个 Racket 函数都有例外抛出机制,和 Java 语言非常类似,对例外抛出的处理在 Racket 的引擎中支持的非常好。这是 Racket 语言非常独特、非常新颖的主要的三点。

关于 Racket 的软件包提供了两种形式。一种是交互式,有点像传统的历史补丁解释器,我们叫做 REPL 循环,即 Read Evaluate Print Loop,系统可以向用户提供一个提示符,在系统提示符下用户可以输入求值的表达式,Racket 引擎会对表达式进行求值,并将求值的结果打印出来,最后等待用户下一次的输入。这个循环对开发人员来说非常的方便,因为编写的程序是否正确马上就可以从系统中得到答案,这是一种方式;还有一种方式,Racket 可以把一个源文件中的源程序直接编译成可执行代码,它提供了这样的功能,或者说产生一种字节码,这种格式可以在安装有 Racket 的不同机器上跑,这个概念有点像 Java 虚拟机上的字节码。所以从性能的角度来看,今天的 Racket 的性能非常好,因为可以直接产生机器码,而且在产生机器码的过程中 Racket 设计小组使用了 JIT(just-in-time)这种编译技术,也就是说,Racket 的字节码会被动态的编译成为原生的机器码,即针对不同的硬件体系结构产生的字节码,在这种条件下,Racket 的性能可以非常的优美。我建议大家安装 Emacs 中专门为 Racket 语言提供的 Major Mode,安装 Major Mode 后在 Emacs 中编程时会非常的漂亮,当然诸位也可以像 Racket 的工程师们期待的那样使用 Racket 的图形用户界面 DrRacket 来运行 Racket,注意在运行 DrRacket 的时候要选择使用什么样的语言,Racket 被定义为 Programmable Programming Language,即可编程的编程语言,请大家注意这个说法,这是其一个典型的特征,这个特征来自于我刚才说过的典型特征中的宏系统,这个宏系统在 6.3 以前和以后的版本中相比,在内部实现上有很多重大的差异,从 6.3 这个版本开始 Racket 小组把宏的机制采用基于集合的运算来做。在这个地方我要解释一下什么叫做宏,宏在 Lisp 或者 Racket、Scheme 中叫做 Macro,Macro 是用于产生新的特殊表(Special Form),这是它的根本目的,在有了特殊表之后,用户即 Racket 程序员可以自己去定义新的语言、语法结构,我刚才说 Racket 被称做 Programmable Programming Language,其原因也是因为它提供了宏。 宏的处理过程其实是这样,首先会把 Racket 源程序编译成为一种中间格式,这种中间格式非常妙,其既可以进一步编译成为中间的字节码,同时又可以导回到源代码形式上去,导回到源代码形式上去的时候它会指出这个宏来自于哪个文件的哪个部分, 包括 Racket 的 Reader 是从源文件的哪个地方开始读取的,我们前面讲过,Racket 是支持词法辖域的,关于词法辖域的一些信息就保留在宏里面,这个是非常方便于程序员开发程序的,所以有了这个中间格式的代码既可以进一步编译成字节码又可以导回至源代码形式的 机构后,宏的内部实现机理就非常的灵活。在 Racket 中有 Template、Pattern 这样的一些概念和术语,具体的、详细的细节请大家仔细阅读 6.3 版的 Racket 文档,Racket 的文档写的非常好,它是和 Racket 的源代码一并发行,并且可以在浏览器中以 HTML 的形式显示出来,而且它内置了一个搜索引擎,因此对于简单的问题大家可以看看指南,对于复杂的问题,例如某一个函数或句法的详细细节,可以参考 Reference,还有在其它一些包中也有相应的文档,可以打开 Plt-help 这个程序,它会自动启动一个浏览器的网页,用于显示 Racket 的所有文档。

前面我提到了合同的机制,是 Racket 的三个特征之一,其实合同是根据模块结构做出来的,有了模块结构后,我们可以使用 Racket 设计大型程序,而模块化的方法是我们应对大型程序复杂性的一种有效的手段,一个模块在 Racket 中可以进行单独的编译以及单独的测试、调试,整个系统的模块也可以被组织起来,进行一并的编译,Racket 还提供了一个非常有用的命令行工具,叫做 Raco,这个 Raco 可以动态的从网上,即 Racket 的 Repository 中下载很多第三方的包以及工具等等,这就弥补了 R5RS 中的许多关于库的不足,有了这些机制之后,Racket 的编程可以做到非常高效,这是由内在的模块机制和相应的合同机制来保证的,而有了 Custodian 这样的特性之后,在 Racket 中内置支持多线程,这一点和 Java 语言较为类似,但与 Java 语言不同的是,Racket 的开发并不需要编译的过程,即不像 Java 中两步分开,Racket 中编译出的代码马上就可以投入到系统中使用,Java 目前还做不到这一点。有了多线程的支持之后,我们就可以在服务器上开发支持多线程的 Daemon Process,这是本书第 7 章的主题。事实上,我们有了这套机制后,我们可以使用 Racket 进行快速建模,从建模方面而言,Racket 是一个非常理想的工具,验证一个概念是否可行,我们可以通过快速的建模来进行验证,如果概念不可行,可以马上停止,在创业方法中我们称其为快速失败,通过快速失败可以节省创业成本。